package com.meida.common.lock;

/**
 * @author flyme
 * @date 2019/10/26 18:29
 */

import com.meida.common.base.utils.FlymeUtils;
import com.meida.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import static java.util.concurrent.Executors.newScheduledThreadPool;

/**
 * 基于Redisson实现分布式锁
 *
 * @author 2019年4月3日
 * <p>
 * {@link https://github.com/redisson/redisson/wiki}
 */
@Component
@Slf4j
public class RedissonLockerImpl implements RedissonLocker {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;


    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String DELIMITER = "|";

    /**
     * 如果要求比较高可以通过注入的方式分配
     */
    private static final ScheduledExecutorService EXECUTOR_SERVICE = newScheduledThreadPool(10);


    /**
     * key 值是否存在
     *
     * @param key
     * @return
     */
    public boolean existKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /************************** 可重入锁 **************************/

    /**
     * 拿不到lock就不罢休，不然线程就一直block 没有超时时间,默认30s
     *
     * @param lockKey
     * @return
     */
    @Override
    public RLock lock(Object lockKey) {
        RLock lock = redissonClient.getLock(lockKey.toString());
        lock.lock();
        return lock;
    }

    /**
     * 自己设置超时时间
     *
     * @param lockKey 锁的key
     * @param timeout 秒 如果是-1，直到自己解锁，否则不会自动解锁
     * @return
     */
    @Override
    public RLock lock(String lockKey, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, TimeUnit.SECONDS);
        return lock;
    }

    /**
     * 自己设置超时时间
     *
     * @param lockKey 锁的key
     * @param unit    锁时间单位
     * @param timeout 超时时间
     */
    @Override
    public RLock lock(String lockKey, TimeUnit unit, int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    /**
     * 尝试加锁，最多等待waitTime，上锁以后leaseTime自动解锁
     *
     * @param lockKey 锁key
     * @param unit    锁时间单位
     * @return 如果获取成功，则返回true，如果获取失败（即锁已被其他线程获取），则返回false
     */
    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, LockConstant lockTime) {
        RLock lock = redissonClient.getLock(lockKey);
        return checkLok(lockKey, unit, lockTime, lock);
    }

    /************************** 公平锁 **************************/
    /**
     * 尝试加锁，最多等待waitTime，上锁以后leaseTime自动解锁
     *
     * @param lockKey 锁key
     * @param unit    锁时间单位
     * @return 如果获取成功，则返回true，如果获取失败（即锁已被其他线程获取），则返回false
     */
    @Override
    public boolean fairLock(String lockKey, TimeUnit unit, LockConstant lockTime) {
        return fairLock(lockKey, unit, lockTime.getLeaseTime());
    }

    private boolean checkLok(String lockKey, TimeUnit unit, LockConstant lockTime, RLock fairLock) {
        try {
            boolean existKey = existKey(lockKey);
            // 已经存在了，就直接返回
            if (existKey) {
                return false;
            }
            return fairLock.tryLock(lockTime.getWaitTime(), lockTime.getLeaseTime(), unit);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 尝试加锁，最多等待waitTime，上锁以后leaseTime自动解锁
     *
     * @param lockKey 锁key
     * @param unit    锁时间单位
     * @return 如果获取成功，则返回true，如果获取失败（即锁已被其他线程获取），则返回false
     */
    @Override
    public boolean fairLock(String lockKey, TimeUnit unit, int leaseTime) {
        RLock fairLock = redissonClient.getFairLock(lockKey);
        try {
            boolean existKey = existKey(lockKey);
            // 已经存在了，就直接返回
            if (existKey) {
                return false;
            }
            return fairLock.tryLock(3, leaseTime, unit);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param lockKey 锁key
     */
    @Override
    public void unlock(String lockKey) {
        try {
            RLock lock = redissonClient.getLock(lockKey);
            lock.unlock();
        } catch (Exception e) {
        }
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock(RLock lock) {
        try {
            lock.unlock();
        } catch (Exception e) {
        }
    }


    /**
     * 获取锁（存在死锁风险）
     *
     * @param lockKey lockKey
     * @param value   value
     * @param time    超时时间
     * @param unit    过期单位
     * @return true or false
     */
    @Override
    public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
        return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
    }


    /**
     * 获取锁
     *
     * @param lockKey lockKey
     * @param uuid    UUID
     * @param timeout 超时时间
     * @param unit    过期单位
     * @return true or false
     */
    public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {
        final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
        boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
        if (success) {
            stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);
        } else {
            String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
            final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
            if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
                return true;
            }
        }
        return success;
    }


    /**
     * 加锁
     *
     * @param targetId  targetId - 商品的唯一标志
     * @param timeStamp 当前时间+超时时间 也就是时间戳
     * @return
     */
    public boolean lock(String targetId, String timeStamp) {
        if (stringRedisTemplate.opsForValue().setIfAbsent(targetId, timeStamp)) {
            // 对应setnx命令，可以成功设置,也就是key不存在
            return true;
        }

        // 判断锁超时 - 防止原来的操作异常，没有运行解锁操作  防止死锁
        String currentLock = stringRedisTemplate.opsForValue().get(targetId);
        // 如果锁过期 currentLock不为空且小于当前时间
        if (FlymeUtils.isNotEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()) {
            // 获取上一个锁的时间value 对应getset，如果lock存在
            String preLock = stringRedisTemplate.opsForValue().getAndSet(targetId, timeStamp);

            // 假设两个线程同时进来这里，因为key被占用了，而且锁过期了。获取的值currentLock=A(get取的旧的值肯定是一样的),两个线程的timeStamp都是B,key都是K.锁时间已经过期了。
            // 而这里面的getAndSet一次只会一个执行，也就是一个执行之后，上一个的timeStamp已经变成了B。只有一个线程获取的上一个值会是A，另一个线程拿到的值是B。
            if (FlymeUtils.isNotEmpty(preLock) && preLock.equals(currentLock)) {
                // preLock不为空且preLock等于currentLock，也就是校验是不是上个对应的商品时间戳，也是防止并发
                return true;
            }
        }
        return false;
    }


    /**
     * 加锁默认超时时间
     */
    private static final long DEFAULT_TIMEOUT_SECOND = 45;

    /**
     * 加锁循环等待时间

     private static final long LOOP_WAIT_TIME_MILLISECOND = 10;   */
    /**
     * 加锁
     *
     * @param key
     * @param timeoutSecond 如果为null,使用默认超时时间
     * @return 加锁的值（超时时间）
     */
    @Override
    public long lock(String key, Long timeoutSecond) {
        log.error("Thread：" + Thread.currentThread().getName() + " 获取锁开始");
        //如果参数错误
        if (timeoutSecond != null && timeoutSecond <= 0) {
            timeoutSecond = DEFAULT_TIMEOUT_SECOND;
        }
        timeoutSecond = timeoutSecond == null ? DEFAULT_TIMEOUT_SECOND : timeoutSecond;
        while (true) {
            //超时时间点
            long timeoutTimeMilli = currentTimeMilliForRedis() + timeoutSecond * 1000;
            //如果设置成功
            if (redisTemplate.opsForValue().setIfAbsent(key, timeoutTimeMilli)) {
                //   log.error("Thread：" + Thread.currentThread().getName() + " 获取锁成功");
                return timeoutTimeMilli;
            }
            //如果已经超时
            Long value = (Long) redisTemplate.opsForValue().get(key);
            if (value != null && value.longValue() < currentTimeMilliForRedis()) {
                //设置新的超时时间
                //旧的值
                Long oldValue = (Long) redisTemplate.opsForValue().getAndSet(key, timeoutTimeMilli);
                //多个线程同时getset，只有第一个才可以获取到锁
                if (value.equals(oldValue)) {
                    // log.error("Thread：" + Thread.currentThread().getName() + " 获取锁成功");
                    return timeoutTimeMilli;
                }
            }
            Random ra = new Random();
            int loop = ra.nextInt(10) + 5;
            //延迟一定毫秒，防止请求太频繁
            try {
                Thread.sleep(loop);
            } catch (InterruptedException e) {
                log.error("线程休眠失败", e);
            }
        }
    }


    /**
     * redis服务器时间
     *
     * @return
     */
    private long currentTimeMilliForRedis() {
        return (Long) redisTemplate.execute(new RedisCallback<Object>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.time();
            }
        });
    }


    /**
     * 释放锁
     *
     * @param key
     * @param lockValue
     */
    public void unLock(String key, long lockValue) {
        Long value = (Long) redisTemplate.opsForValue().get(key);
        //如果是本线程加锁
        if (value != null && value.equals(lockValue)) {
            redisTemplate.delete(key);
        }
    }

    /**
     * 解锁
     *
     * @param target
     * @param timeStamp
     */
    public void unlock(String target, String timeStamp) {
        try {
            String currentValue = stringRedisTemplate.opsForValue().get(target);
            if (FlymeUtils.isNotEmpty(currentValue) && currentValue.equals(timeStamp)) {
                // 删除锁状态
                stringRedisTemplate.opsForValue().getOperations().delete(target);
            }
        } catch (Exception e) {
            log.error("解锁异常{}", e);
        }
    }


    /**
     * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
     */
    public void unlock2(String lockKey, String value) {
        unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
    }

    /**
     * 延迟unlock
     *
     * @param lockKey   key
     * @param uuid      client(最好是唯一键的)
     * @param delayTime 延迟时间
     * @param unit      时间单位
     */
    public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
        if (StringUtils.isEmpty(lockKey)) {
            return;
        }
        if (delayTime <= 0) {
            doUnlock(lockKey, uuid);
        } else {
            EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
        }
    }

    /**
     * @param lockKey key
     * @param uuid    client(最好是唯一键的)
     */
    private void doUnlock(final String lockKey, final String uuid) {
        String val = stringRedisTemplate.opsForValue().get(lockKey);
        final String[] values = val.split(Pattern.quote(DELIMITER));
        if (values.length <= 0) {
            return;
        }
        if (uuid.equals(values[1])) {
            stringRedisTemplate.delete(lockKey);
        }
    }


}
